import copy
from dlgo.gotypes import Player

'''
这个类是一个描述行为的类，我们的一个回合有三个行为，落子，跳过，认输。这里的类函数用来返回一个包含某个行为的对象
'''
class Move:
    def __init__(self, point=None, is_pass=False, is_resign=False):
        # assert point is not None or is_pass or is_resign
        # 更安全的 assert，书上的代码只能判断不能全为false,但实际情况有可能有两个以上的true,所以这里最佳情况是只有一个是true
        assert sum([
            point is not None,
            is_pass,
            is_resign
        ]) == 1, "Move must be exactly one of: play, pass, or resign"
        self.point = point
        self.is_play = (self.point is not None)
        self.is_pass = is_pass
        self.is_resign = is_resign

    #这里用类方法，这些方法相当于工厂函数，不需要实例化就可以调用，并且返回一个包含特定行为的Move类
    @classmethod
    def play(cls, point):
        return Move(point=point)

    @classmethod
    def pass_turn(cls):
        return Move(is_pass=True)

    @classmethod
    def resign(cls):
        return Move(is_resign=True)



'''
描述一个棋块
有颜色，气集合，棋子集合
里面有维护气的函数
'''
class GoString:
    def __init__(self, color, stones, liberties):
        self.color = color
        self.stones = set(stones)
        # 气set集合
        self.liberties = set(liberties)

    # 将一个气删除，因为point是继承自namedtuple,所以可以直接用remove之类的函数，而不用在point类里面自己重写比较和hash函数
    def remove_liberty(self, point):
        self.liberties.remove(point)

    # 添加一个气
    def add_liberty(self, point):
        self.liberties.add(point)

    # 合并两个棋块
    # TODO 合并的逻辑是否有问题，合并气的时候是不是只合并并集且去除交集，这个还要看后面的代码怎么写
    def merge_with(self, go_string):
        # 判断颜色是否相等
        assert go_string.color == self.color
        combined_stones = self.stones | go_string.stones
        combined_liberties = (self.liberties | go_string.liberties) - combined_stones
        return GoString(self.color, combined_stones, combined_liberties)

    @property
    # 返回气数
    def num_liberties(self):
        return len(self.liberties)

    # 比较函数，两个棋块是否一样
    def __eq__(self, other):
        return (
            isinstance(other, GoString) and
            self.color == other.color and
            self.stones == other.stones and
            self.liberties == other.liberties
        )



class Board:
    """
    棋盘类
    """

    def __init__(self, num_rows, num_cols):
        self.num_rows = num_rows
        self.num_cols = num_cols
        # 存储棋块的字典，key是点，value是棋块
        self._grid = {}

    def place_stone(self, player, point):
        """下一个棋子函数，实现了落下一个子后，可能会提起子的功能，并且可以维护棋盘上的棋块
        :param player: 棋手
        :param point: 落子点
        :return: 无
        这个函数非常的关键，要理解他的逻辑
        """
        # 判断点是否在棋盘内
        assert self.is_on_grid(point)
        # 判断点是否已经落子
        assert self._grid.get(point) is None
        # 相邻同色棋块
        adjacent_same_color = []
        # 相邻不同色棋块
        adjacent_opposite_color = []
        # 相邻气列表
        liberties = []
        # 遍历下子的周围的四个点
        for neighbor in point.neighbors():
            #如果不在棋盘就跳过
            if not self.is_on_grid(neighbor):
                continue
            # 得到这个邻居点的棋块
            neighbor_string = self._grid.get(neighbor)
            if neighbor_string is None:
                # 如果没有属于棋块，说明这个地方是个空点，那么我们就要添加一个气
                liberties.append(neighbor)
            elif neighbor_string.color == player:
                # 如果有棋块并且这个邻居的颜色和下的棋一样，那么我们就要把这个相邻同色棋块添加到一个相邻同色棋块列表里面
                if neighbor_string not in adjacent_same_color:
                    # 将这个邻居棋块添加到一个相邻同色棋块里面
                    adjacent_same_color.append(neighbor_string)
            else:
                # 如果有棋块并且这个邻居的颜色和下的棋不一样，那么我们就要将这个邻居棋块添加到一个相邻不同色棋块列表里面
                if neighbor_string not in adjacent_opposite_color:
                    adjacent_opposite_color.append(neighbor_string)

        #生成一个新的棋块，此时棋块的棋子集合只有我们下的这个点，后面还要继续完善
        new_string = GoString(player, [point], liberties)

        # 遍历相邻同色棋块，将棋块合并在一起，生成一个新的棋块
        for same_color_string in adjacent_same_color:
            new_string = new_string.merge_with(same_color_string)
        # 遍历这个新棋块的每个点，更新这个棋盘的棋块字典
        for new_point in new_string.stones:
            self._grid[new_point] = new_string
        # 遍历相邻不同色棋块，将棋块气数减一
        # 这里稍微修改了一下书上的代码将两个循环进行了合并
        for other_color_string in adjacent_opposite_color:
            other_color_string.remove_liberty(point)
            # 遍历相邻不同色棋块，如果棋块的气数等于0，那么将这个棋块从棋盘字典中删除
            if other_color_string.num_liberties == 0:
                self._remove_string(other_color_string)








    def is_on_grid(self, point):
        """判断点是否在棋盘内
        :param point: 点
        :return: 是否在棋盘内
        """
        return (
                1 <= point.row <= self.num_rows and
                1 <= point.col <= self.num_cols
        )

    def get(self, point):
        """返回点的颜色
        :param point: 点
        :return: 棋块的颜色对象
        """
        string = self._grid.get(point)
        if string is None:
            return None
        return string.color


    def get_go_string(self, point):
        """返回点的棋块对象
        :param point:  点
        :return: 棋块对象
        """
        string = self._grid.get(point)
        if string is None:
            return None
        return string

    def _remove_string(self, string):
        """将一个棋块从字典中删除
        :param string: 棋块对象
        :return: 无
        """
        #遍历棋块的每个点
        for point in string.stones:
            #遍历这个点周围的四个点
            for neighbor in point.neighbors():
                #得到这个点的棋块
                neighbor_string = self._grid.get(neighbor)
                if neighbor_string is None:
                    #如果没有棋块，跳过
                    continue
                if neighbor_string is not string:
                    #如果有棋块，并且这个棋块不是我们删除的棋块，那么将这个棋块的气数加一，因为提走后可能其他棋块的气会增加
                    neighbor_string.add_liberty(point)
            #从字典中删除这个点（这个key还在，只是值为None）
            self._grid[point] = None







class GameState:
    """
    存储围棋的游戏状态类
    """

    def __init__(self, board, next_player, previous, move):
        self.board = board
        self.next_player = next_player
        self.previous_state = previous
        self.last_move = move

    def apply_move(self, move):
        """根据这一回合的行为，来返回一个新的游戏状态对象
        :param move: 落子点
        :return: 新的游戏状态对象
        """
        if move.is_play:
            # 如果要落子，就调用棋盘对象的落子函数进行落子
            next_board = copy.deepcopy(self.board)
            next_board.place_stone(self.next_player, move.point)
        else:
            # 如果不落子，就维持当前的棋盘状态
            next_board = self.board
        return GameState(next_board, self.next_player.other, self, move)

    def is_over(self):
        """判断游戏是否结束
        :return: 是否结束
        """
        if self.last_move is None:
            # 如果是游戏的开局状态（黑方还没有落下第一个子）
            return False
        if self.last_move.is_resign:
            # 如果上一回合认输，那么游戏结束
            return True
        second_last_move = self.previous_state.last_move
        if second_last_move is None:
            # 这个是判断黑棋的第一步决策之后的状态，因为黑棋第一步后的上上一步的Move是none，是一个特殊情况，所以我们要写这个判断
            return False
        # 如果上一回合和上一次上一回合都没有落子，那么游戏结束
        return second_last_move.is_pass and self.last_move.is_pass

    def is_move_self_capture(self, player, move):
        """判断准备下的棋是否产生自吃，逻辑是先模拟下一步，然后看自己还又没气。这里面不用实现下了之后提子的逻辑，因为place_stone已经实现了
        :param player: 玩家
        :param move: 行为
        """
        if not move.is_play:
            # 如果这个行为不是落子行为，那么我们返回False
            return False
        # 我们复制一下当前的棋盘，用来模拟下棋后会不会自吃
        next_board = copy.deepcopy(self.board)
        # 模拟下棋
        next_board.place_stone(player, move.point)
        # 得到这个点下的棋块
        new_string = next_board.get_go_string(move.point)
        # 如果下了之后这个棋块没气了，就说明子吃了
        return new_string.num_liberties == 0

    def does_move_violate_ko(self, player, move):
        """判断是否会劫争
        :param player: 玩家
        :param move: 行为
        :return: 是否会劫
        这个函数的逻辑就是模拟下一步棋，然后看这一部棋之后的状态和之前所有游戏状态是否一样，如果一样，那么就会出现劫争
        这里的劫争规则运用的是全局劫争规则，不同于单劫争规则，这样更加严谨，可以避免死循环的存在
        """
        if not move.is_play:
            #如果不是落子行为，那么我们返回False
            return False
        # 得到一个模拟的复制棋盘
        next_board = copy.deepcopy(self.board)
        # 模拟下一步棋
        next_board.place_stone(player, move.point)
        # 得到下了一步棋之后的状态
        next_situation = (player.other, next_board)
        # 上一步的游戏状态
        past_state = self.previous_state
        #一直遍历历史状态
        while past_state is not None:
            # 遍历上上步的状态
            if past_state.situation == next_situation:
                # 如果这个状态和模拟的棋盘状态一样，那么说明出现了劫
                return True
            # todo这里是否可以直接优化一下，直接跳到上上步的状态，然后判断是否和模拟的棋盘状态一样，如果一样，那么说明出现了劫
            # 思考了一下，如果这样优化会增加代码的复杂度和降低可读性，并且一般我们的棋局只有300手左右，带来的优化速度微乎其微，所以沿用书上的
            past_state = past_state.previous_state
        return  False

    def is_valid_move(self, move):
        """判断行为是否合法
        :param move: 行为
        """
        if self.is_over():
            # 如果游戏已经结束，那么我们返回False
            return False
        if move.is_pass or move.is_resign:
            # 如果是过棋或者认输，那么我们返回True
            return True
        return (
            # 这个点没有棋子，并且不会产生自吃现象（确保可能提子后），并且不会有劫争问题，就是合法的
            self.board.get(move.point) is None and
            not self.is_move_self_capture(self.next_player, move) and
            not self.does_move_violate_ko(self.next_player, move)
        )


    @property
    def situation(self):
        """返回当前游戏状态的 situation
        :return: 当前游戏状态的 situation，包含下一个棋手和当前棋盘状态的元组
        因为有劫争的规则，就是下一步棋之后，不能出现上上一步的状态，所以这里要记录一下状态，用来判断是否产生劫争
        """
        return self.next_player, self.board



    @classmethod
    def new_game(cls, board_size):
        """工厂方法，用来创建游戏的初始状态对象，可以设置棋盘的大小，就是黑子准备下，但是还没下的状态
        :param board_size: 棋盘大小
        :return: 新的游戏状态
        """
        if isinstance(board_size, int):
            board_size = (board_size, board_size)
        board = Board(*board_size)
        return GameState(board, Player.black, None, None)











